package cn.zyjblogs.filter;

import cn.zyjblogs.config.replay.AntiReplayProperties;
import cn.zyjblogs.exception.AntiReplayException;
import cn.zyjblogs.starter.common.autoconfigure.rsa.RsaKeyProperties;
import cn.zyjblogs.starter.common.entity.constant.CommonRedisKeyConstant;
import cn.zyjblogs.starter.common.entity.constant.ContextKeyConstant;
import cn.zyjblogs.starter.common.entity.constant.HttpHeaderConstant;
import cn.zyjblogs.starter.common.entity.context.BaseContext;
import cn.zyjblogs.starter.common.entity.dto.ContextDto;
import cn.zyjblogs.starter.common.entity.response.HttpCode;
import cn.zyjblogs.starter.common.entity.response.ResponseObject;
import cn.zyjblogs.starter.common.entity.response.ResponseResult;
import cn.zyjblogs.starter.common.utils.jwt.JwtParsers;
import cn.zyjblogs.starter.common.utils.rsa.RsaUtils;
import cn.zyjblogs.starter.common.utils.string.StringUtils;
import cn.zyjblogs.starter.redis.utils.RedisTemplateHandler;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.CollectionUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import javax.validation.constraints.NotNull;
import java.nio.charset.StandardCharsets;
import java.nio.file.Paths;
import java.security.PublicKey;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * Copyright (C), 2021, 北京同创永益科技发展有限公司
 *
 * @author tanyuanzhi
 * @version 3.0.0
 * description token校验过滤器，要在WebsocketRoutingFilter过滤器之后huo
 * @date 2021/11/1 16:22
 */
@Component
@Slf4j
@EnableConfigurationProperties({WhiteListProperties.class, AntiReplayProperties.class})
@RequiredArgsConstructor
public class AuthFilter implements GlobalFilter {
    private final WhiteListProperties whiteListProperties;
    private AntPathMatcher antPathMatcher = new AntPathMatcher();
    private final RsaKeyProperties rsaKeyProperties;
    private final RedisTemplateHandler<String, String> redisTemplateHandler;
    private final AntiReplayProperties antiReplayProperties;

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        //添加网关标记
        ServerHttpRequest serverHttpRequest = exchange.getRequest().mutate()
                .headers(h -> h.add(HttpHeaderConstant.REQUEST_FROM_GATEWAY_KEY, HttpHeaderConstant.REQUEST_FROM_GATEWAY_VALUE))
                .build();
        ServerWebExchange build = exchange.mutate().request(serverHttpRequest).build();
        ServerHttpRequest request = build.getRequest();
        ServerHttpResponse response = build.getResponse();
        String token = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
        String path = request.getURI().getPath();
        //白名单请求直接放行
        if (isWhileList(path)) {
            //重放攻击防护
            return antiReplayFilter(exchange, chain);
        }
        if (token == null || StringUtils.isEmpty(token)) {
            return getErrorMono(response, HttpCode.UNAUTHORIZED, "无访问权限");
        }
        if (!token.toLowerCase().startsWith(HttpHeaderConstant.BEARER_TYPE.toLowerCase())) {
            return getErrorMono(response, HttpCode.UNAUTHORIZED, "令牌格式有误");
        }
        if (isExpired(token)) {
            log.info("token过期");
            return getErrorMono(response, HttpCode.UNAUTHORIZED, "token失效");
        }
        //重放攻击防护
        return antiReplayFilter(build, chain);
    }

    public Mono<Void> antiReplayFilter(ServerWebExchange exchange, GatewayFilterChain chain) {
        if (Boolean.FALSE.equals(antiReplayProperties.getEnabled())) {
            return chain.filter(exchange);
        }
        ServerHttpRequest request = exchange.getRequest();
        HttpHeaders headers = request.getHeaders();
        String nonce = headers.getFirst(antiReplayProperties.getNonce());
        String timestamp = headers.getFirst(antiReplayProperties.getTimestamp());
        String sign = headers.getFirst(antiReplayProperties.getSign());
        if (StringUtils.isEmpty(nonce) || StringUtils.isEmpty(timestamp) || StringUtils.isEmpty(sign)) {
            return getErrorMono(exchange.getResponse(), HttpCode.BAD_REQUEST, "请求头参数错误!");
        }
        try {
            validateNonceAndTimestamp(nonce, timestamp, request);
        } catch (Exception e) {
            return getErrorMono(exchange.getResponse(), HttpCode.BAD_REQUEST, e.getMessage());
        }
        return chain.filter(exchange);
    }

    /**
     * 校验
     *
     * @param nonce
     * @param timestamp
     * @param request
     * @throws Exception
     */
    private void validateNonceAndTimestamp(String nonce, String timestamp, ServerHttpRequest request) throws Exception {
        Boolean flag = redisTemplateHandler.hasKey(AntiReplayProperties.REDIS_PREFIX + nonce);
        // 验证Nonce是否已经使用过
        if (Boolean.TRUE.equals(flag)) {
            throw new AntiReplayException("请重复请求！");
        }
        // 验证Timestamp是否在合理时间范围内
        long timeStampValue;
        try {
            timeStampValue = Long.parseLong(timestamp);
        } catch (NumberFormatException e) {
            throw new AntiReplayException("{} 参数错误!", antiReplayProperties.getTimestamp());
        }
        // 请求传过来的间戳与服务器当前时间戳差值大于120，则当前请求的timestamp无效
        if (Math.abs(timeStampValue - System.currentTimeMillis()) / 1000 > antiReplayProperties.getExpireTime()) {
            throw new AntiReplayException("{} 请求过期!", antiReplayProperties.getTimestamp());
        }
        //校验签名
        SignatureValidator.builder().data(antiReplayProperties, request).execute();
        redisTemplateHandler.set(AntiReplayProperties.REDIS_PREFIX + nonce, timestamp);
        redisTemplateHandler.expire(AntiReplayProperties.REDIS_PREFIX + nonce, antiReplayProperties.getExpireTime(), TimeUnit.SECONDS);
    }

    /**
     * token是否过期
     *
     * @param token token
     * @return 过期true
     * @author tanyuanzhi
     * @date 2021/11/15 19:17
     */
    private boolean isExpired(String token) {
        String jwt = token.substring(HttpHeaderConstant.BEARER_TYPE.length()).trim();
        try {
            JwtParsers jwtParsers = new JwtParsers(true);
            if (Boolean.TRUE.equals(rsaKeyProperties.getEnable())) {
                String publicKeyStr = redisTemplateHandler.get(CommonRedisKeyConstant.REDIS_KEY_PUBLIC_RSA);
                if (StringUtils.isEmpty(publicKeyStr)) {
                    publicKeyStr = IOUtils.toString(Paths.get(rsaKeyProperties.getPubKeyPath()).toUri(), StandardCharsets.UTF_8);
                }
                PublicKey publicKey = RsaUtils.getPublicKeyByKeyStr(publicKeyStr);
                jwtParsers.setSigningKey(publicKey);
            }
            Claims body = jwtParsers.parseClaimsJws(jwt).getBody();
            String userId = (String) body.get(ContextKeyConstant.USER_ID_KEY);
            String username = (String) body.get(ContextKeyConstant.USERNAME_KEY);
            String tenantId = (String) body.get(ContextKeyConstant.TENANT_ID_KEY);
            BaseContext.set(ContextDto.builder().userId(userId).username(username).token(token).tenantId(tenantId).build());
            return false;
        } catch (ExpiredJwtException e) {
            log.info("token过期{}", token);
            return true;
        } catch (Exception e) {
            log.info("token无效{}", token);
            return true;
        }
    }


    /**
     * 是否是白名单
     *
     * @param path 当前路径
     * @return 是否是白名单中的地址
     * @author tanyuanzhi
     * @date 2021/11/2 9:15
     */

    private boolean isWhileList(String path) {
        List<String> allowPaths = whiteListProperties.getAllowPaths();
        if (CollectionUtils.isEmpty(allowPaths)) {
            return false;
        }
        return allowPaths.stream().anyMatch(allowPath -> antPathMatcher.match(allowPath, path));

    }


    /**
     * 验证不通过时的返回
     *
     * @param response     响应
     * @param responseCode 响应码
     * @return reactor.core.publisher.Mono<java.lang.Void>
     * @date 2021/11/1 17:10
     */
    private Mono<Void> getErrorMono(ServerHttpResponse response, HttpCode responseCode, String msg) {
        response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
        response.setStatusCode(org.springframework.http.HttpStatus.OK);
        return getVoidMono(response, responseCode, msg, log);
    }

    @NotNull
    static Mono<Void> getVoidMono(ServerHttpResponse response, HttpCode responseCode, String msg, Logger log) {
        ResponseObject<Object> responseObject = ResponseResult.error(responseCode, msg);
        ObjectMapper mapper = new ObjectMapper();
        byte[] bytes = new byte[0];
        try {
            bytes = mapper.writeValueAsBytes(responseObject);
        } catch (JsonProcessingException e) {
            log.error("转换错误");
        }
        DataBuffer wrap = response.bufferFactory().wrap(bytes);
        return response.writeWith(Mono.just(wrap));
    }
}
